package uk.co.q3c.deplan.client.domain.resource;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.persistence.Id;
import javax.persistence.Transient;

import uk.co.q3c.deplan.client.domain.AbstractDomainObject;
import uk.co.q3c.deplan.client.domain.DomainKind;
import uk.co.q3c.deplan.client.domain.PropertyChangeListener;
import uk.co.q3c.deplan.client.domain.StructureChangeListener;
import uk.co.q3c.deplan.client.domain.StructureChangeNotifier;
import uk.co.q3c.deplan.client.service.ServiceFactory;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Cached;
import com.googlecode.objectify.annotation.Unindexed;

/**
 * Represents a pool of resource, "owned" in the sense that only the manager (or delegate) of the pool can assign work to individuals within
 * the pool. However, from a persistence perspective resources within a pool are not owned and must therefore be explicitly saved when
 * created or modified
 * 
 * @author David Sowerby 15 Feb 2010
 * 
 */
@Unindexed
@Cached(expirationSeconds = 600)
public class ResourcePool extends AbstractDomainObject implements Serializable {
	protected static final Logger logger = Logger.getLogger(ResourcePool.class.getName());
	private static final long serialVersionUID = 1L;

	@Id
	private Long keyId;

	private String defaultResourceName;
	@Transient
	transient List<StructureChangeListener> structureChangeListeners;

	private List<Key<ResourceGroup>> groupsKeys = new ArrayList<Key<ResourceGroup>>();

	private List<Key<IndividualResource>> individualsKeys = new ArrayList<Key<IndividualResource>>();

	@Transient
	private List<IndividualResource> individuals;
	@Transient
	private List<ResourceGroup> groups;

	private Key<ResourcePool> key;

	protected ResourcePool() {
		super();
	}

	public ResourcePool(ServiceFactory connection) {
		super(connection);
	}

	/**
	 * Creates #structureChangeListeners
	 * 
	 * @see AbstractPersistedObject#beforeLoad
	 */
	@Override
	public void beforeLoad() {
		structureChangeListeners = new ArrayList<StructureChangeListener>();
	}

	/**
	 * Looks to see if there is a group already containing the specified individuals (and only them) - if not, one is created with a unique
	 * temporary name. If any of the <code>individuals</code> are not already in the pool, or is null, a runtime exception is raised and
	 * null returned.
	 * 
	 * @exception InvalidResourceException
	 *                if any one of the <code>individuals</code> are null or not in the pool
	 * @param individuals
	 * @return
	 */
	public ResourceGroup groupContainingOnlyKeys(List<Key<IndividualResource>> keyList) {
		ResourceGroup result = null;
		// make sure all the resources are in the pool

		if (keyList.size() > individualsKeys.size()) {
			throw new InvalidResourceException("Group can only contain resource from the resource pool");
		}
		for (Key<IndividualResource> resourceKey : keyList) {
			if (!individualsKeys.contains(resourceKey)) {
				break;
			}
		}

		boolean found = false;
		// have to expand group keys for this
		for (ResourceGroup group : groups()) {
			found = group.containsAllAndOnlyKeys(keyList);
			if (found) {
				result = group;
				break;
			}
		}

		// if (result == null) {
		// ResourceGroup newGroup = new ResourceGroup(getConnection());
		// newGroup.setName(createUniqueName());
		// result = newGroup;
		// result.setResourcesKeys(keyList);
		// // need to save the new group so it has a key
		// result.save();
		// groupsKeys.add(result.getKey());
		// groups.add(result);
		// }
		return result;

	}

	/**
	 * Gets the keys for the supplied resources and calls {@link #groupContainingOnlyKeys(List)}
	 * 
	 * @see #groupContainingOnlyKeys(List)
	 * 
	 * @param individuals
	 * @return
	 * @throws PersistenceReferenceException
	 */
	public ResourceGroup groupContainingOnly(List<IndividualResource> individuals) throws PersistenceReferenceException {
		List<Key<IndividualResource>> keys = new ArrayList<Key<IndividualResource>>();
		for (IndividualResource resource : individuals) {
			keys.add(resource.getKey());
		}
		return groupContainingOnlyKeys(keys);

	}

	public void addResources(IndividualResource[] newResources) throws PersistenceReferenceException {

		for (IndividualResource ir : newResources) {
			addResource(ir);
		}

	}

	public void addResources(ArrayList<Resource> newResources) throws PersistenceReferenceException {
		for (Resource ir : newResources) {
			addResource(ir);
		}

	}

	public ResourceGroup groupContainingOnlyNames(List<String> individualNames) throws PersistenceReferenceException {

		List<Key<IndividualResource>> keys = new ArrayList<Key<IndividualResource>>();
		for (String name : individualNames) {
			IndividualResource r = individualNameLookup(name);
			keys.add(r.getKey());
		}
		return groupContainingOnlyKeys(keys);
	}

	public String createUniqueName() {

		int i = 0;
		String s = null;
		boolean done = false;
		while (!done) {
			s = "unnamed" + Integer.toString(i);
			if (allNameLookup(s) == null) {
				done = true;
			}
			i++;
		}
		return s;
	}

	/**
	 * returns the number of groups defined
	 * 
	 * @return
	 */
	public int groupCount() {

		return groupsKeys.size();
	}

	/**
	 * returns the number of individual resources defined
	 */
	public int individualCount() {

		return individualsKeys.size();
	}

	/**
	 * returns the resource with the specified name. This could be either a group or an individual resource. If the specified resource isn't
	 * found a runtime exception is raised. If a null name is passed, an attempt is made to use the <code>defaultResourceName</code>. If one
	 * hasn't been defined, a runtime exception is raised
	 * 
	 * @param name
	 * @return
	 */
	public Resource getResource(String name) {
		Resource resource = null;
		// if name is null, use default name if possible
		if (name == null) {
			resource = allNameLookup(defaultResourceName);
			if (resource == null) {
				throw new DefaultResourceException("Default Resource not defined");
			}
			return resource;
		}
		resource = allNameLookup(name);

		// name not null
		resource = allNameLookup(name);
		if (resource == null) {
			logger.info("invalid resource requested (" + name + ")");
			throw new InvalidResourceException("resource " + name + " is not defined");
		}
		return resource;
	}

	/**
	 * Adds the specified resource to the pool, can be group or individual
	 * 
	 * @param resource
	 * @throws PersistenceReferenceException
	 */
	@SuppressWarnings("unchecked")
	public void addResource(Resource resource) throws PersistenceReferenceException {

		if (resource == null) {
			logger.warning("Resource is null, ignored");
			return;
		}
		if (resource.getName() == null) {
			RuntimeException e = new RuntimeException("Resource cannot have null name when added to pool");
			logger.info(e.getLocalizedMessage() + e.getStackTrace());
			throw e;
		}
		if (resource instanceof ResourceGroup) {
			groups().add((ResourceGroup) resource);
			groupsKeys.add((Key<ResourceGroup>) resource.getKey());
			fireStructureChange(resource, true);
		} else {
			individuals().add((IndividualResource) resource);
			individualsKeys.add((Key<IndividualResource>) resource.getKey());

		}
	}

	/**
	 * Returns a direct reference to {@link #groups}
	 */
	public List<ResourceGroup> groups() {

		if (groups == null) {
			groups = getConnection().resourceService()
					.groupsFromsKeys(groupsKeys);
		}
		return groups;
	}

	/**
	 * Returns a copy of the list of resource groups held in the pool
	 * 
	 * @see #groupsArray()
	 * 
	 * @return
	 */
	public List<ResourceGroup> getGroups() {

		List<ResourceGroup> list = new ArrayList<ResourceGroup>();
		list.addAll(groups());
		return list;
	}

	/**
	 * Returns a shallow copy of the list of individual resources in the pool
	 * 
	 * @return
	 */
	public List<IndividualResource> getIndividuals() {

		List<IndividualResource> list = new ArrayList<IndividualResource>();
		list.addAll(individuals());
		return list;
	}

	/**
	 * returns a direct reference to the individuals
	 * 
	 * @return
	 */
	public List<IndividualResource> individuals() {
		if (individuals == null) {
			individuals = getConnection().resourceService()
					.individualsFromKeys(individualsKeys);
		}

		return individuals;

	}

	public void setDefaultResourceName(String defaultName) {
		Resource resource = allNameLookup(defaultName);
		if (resource == null) {
			throw new InvalidResourceException(defaultName + " cannot be set as the default resource, it does not exist in the resource pool");
		}
		this.defaultResourceName = defaultName;

	}

	public String getDefaultResourceName() {

		return defaultResourceName;
	}

	public Resource defaultResource() {

		return getResource(defaultResourceName);
	}

	/**
	 * Clears all booked time from all {@link IndividualResource}s, usually prior to levelling.
	 */
	public void clearBookings() {
		// public void clearProfiles(Date startDate, int range) {

		for (IndividualResource ir : individuals()) {
			ir.clearBookings();
		}
	}

	public ResourceGroup[] groupsArray() {

		ResourceGroup[] g = new ResourceGroup[groups().size()];
		int i = 0;
		for (ResourceGroup group : groups()) {
			g[i] = group;
			i++;
		}

		return g;
	}

	/**
	 * passes property change listener to all resources and resource groups, although it doesn't use the listener itself.
	 */
	@Override
	public void addPropertyChangeListener(PropertyChangeListener listener) {

		List<ResourceGroup> groups = getGroups();
		for (ResourceGroup r : groups) {
			r.addPropertyChangeListener(listener);
		}
		List<IndividualResource> inds = getIndividuals();
		for (IndividualResource ir : inds) {
			ir.addPropertyChangeListener(listener);
		}

	}

	/**
	 * Sets the base calendar for all contained {@link IndividualResource}s
	 * 
	 * @param baseCalendar
	 */
	public void setBaseCalendar(BaseCalendar baseCalendar) {

		for (IndividualResource ir : individuals()) {
			ir.setBaseCalendar(baseCalendar);
		}

	}

	@Override
	protected void fireStructureChange(StructureChangeNotifier child, boolean added) {
		for (StructureChangeListener listener : structureChangeListeners) {
			listener.childChanged(child, added);
		}
	}

	@Override
	public void addStructureChangeListener(StructureChangeListener listener) {
		structureChangeListeners.add(listener);
	}

	@Override
	public void removeStructureChangeListener(StructureChangeListener listener) {
		structureChangeListeners.remove(listener);
	}

	@Override
	public DomainKind getKind() {
		return DomainKind.ResourcePool;
	}

	public Long getKeyId() {
		return keyId;
	}

	private Resource allNameLookup(String name) {
		if (name == null) {
			return null;
		}
		Resource result = individualNameLookup(name);
		if (result != null) {
			return result;
		}
		result = groupNameLookup(name);
		return result;
	}

	private ResourceGroup groupNameLookup(String name) {
		ResourceGroup result = null;
		for (ResourceGroup ir : groups()) {
			if (name.equalsIgnoreCase(ir.getName())) {
				result = ir;
				break;
			}
		}
		return result;

	}

	private IndividualResource individualNameLookup(String name) {
		IndividualResource result = null;
		for (IndividualResource ir : individuals()) {
			if (name.equalsIgnoreCase(ir.getName())) {
				result = ir;
				break;
			}
		}
		return result;
	}

	public Key<ResourcePool> getKey() throws PersistenceReferenceException {
		if (getKeyId() == null) {
			throw new PersistenceReferenceException(this.getName() + " has not been saved and cannot therefore be used as a reference");
		}
		if (key == null) {
			key = connection.resourceService()
					.keyFor(this);
		}
		return key;
	}

	public void remove(IndividualResource resource) {
		individuals.remove(resource);
		try {
			individualsKeys.remove(resource.getKey());
		} catch (PersistenceReferenceException e) {
			// hasn't been saved, so won't be in the key list - ignore
		}
	}

	public void remove(ResourceGroup group) {
		groups.remove(group);
		try {
			groupsKeys.remove(group.getKey());
		} catch (PersistenceReferenceException e) {
			// hasn't been saved, so won't be in the key list - ignore
		}
	}

	@Override
	public void setKeyId(Long keyId) {
		throw new RuntimeException("Not yet implemented");
	}

}
